﻿using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Language.Utility;
using Microsoft.VisualStudio.Text;
using Red.Scripts.Defines;
using Defines = Red.Scripts.Defines.Scc;

namespace Language.Scc
{
    internal class Request
    {
        #region Public Properties

        public string Path { get; set; }
        public ulong Hash { get; set; }
        public ITextSnapshot Data { get; set; }
        public string Text { get; set; }

        public bool Started { get; set; }
        public bool Cancelled { get; set; }

        public Task Compilation => CompilationCompletionSource.Task;
        public Task Tokens => TokensCompletionSource.Task;
        public TaskCompletionSource<bool> CompilationCompletionSource { get; private set; } = new TaskCompletionSource<bool>();
        public TaskCompletionSource<bool> TokensCompletionSource { get; private set; } = new TaskCompletionSource<bool>();

        #endregion Public Properties
    }

    internal class LockedRequests
    {
        #region Private Data

        private SemaphoreSlim _lock = new SemaphoreSlim( 1 );
        private Requests _requests = new Requests();

        #endregion Private Data

        #region Public Methods

        public async Task CreateRequest( string file, ITextSnapshot data )
        {
            await _lock.WaitAsync();

            await _requests.CreateRequest( file, data );

            _lock.Release();
        }

        public async Task AwaitInprogressRequests( string file )
        {
            await _lock.WaitAsync();

            Request request = _requests.GetFirstRequest( file, true );

            _lock.Release();

            if ( request?.Started == true )
            {
                await request.Compilation;
                await request.Tokens;
            }

        }

        public async Task SendNextRequest( string file, string root, Defines.Connection connection )
        {
            await _lock.WaitAsync();

            _requests.SendNextRequest( file, root, connection );

            _lock.Release();
        }

        public async Task<Request> CompilationComplete( string file, ulong hash, bool success )
        {
            await _lock.WaitAsync();

            Request request = _requests.CompilationComplete( file, hash, success );

            _lock.Release();

            return request;
        }

        public async Task<Request> TokensComplete( string file, ulong hash, bool success )
        {
            await _lock.WaitAsync();

            Request request = _requests.TokensComplete( file, hash, success );

            _lock.Release();

            return request;
        }

        #endregion Public Methods
    }

    internal class Requests
    {
        #region Private Data

        private Dictionary<string, List<Request>> _requests = new Dictionary<string, List<Request>>();

        #endregion Private Data

        #region Public Methods

        public async Task<Request> CreateRequest( string file, ITextSnapshot data )
        {
            List<Request> requests = GetRequests( file );

            string text = data.GetText();

            ulong hash = await CalculateHash( text );

            Request existing = null;

            foreach( Request request in requests )
            {
                if ( request.Hash == hash )
                {
                    Debug.Assert( existing == null );

                    // There is already a request for this data, do nothing
                    Log( file, "Request already exists for this data" );

                    existing = request;
                }
                else if( !request.Started )
                {
                    if ( existing != null )
                    {
                        request.Cancelled = true;
                        Log( file, "Cancelled existing request as it conflicts with new request" );
                    }
                    else
                    {
                        // We've found an existing request with different data (that isn't already in progress), so let's just overwrite that
                        request.Hash = hash;
                        request.Data = data;
                        request.Text = text;

                        Log( file, "Updated existing request" );

                        existing = request;
                    }
                }
            }

            if ( existing != null )
                return existing;

            // Create a new request
            Request newRequest = new Request()
            {
                Path = file,
                Hash = hash,
                Data = data,
                Text = text
            };

            requests.Add( newRequest );

            Log( file, "Created new request" );
            return newRequest;
        }

        public void SendNextRequest( string file, string root, Defines.Connection connection )
        {
            List<Request> requests = GetRequests( file );

            requests.RemoveAll( r => r.Cancelled );

            Request request = requests.FirstOrDefault( r => !r.Started );

            if ( request == null )
                return;

            Debug.Assert( !request.Started );
            Debug.Assert( !request.Compilation.IsCompleted );
            Debug.Assert( !request.Tokens.IsCompleted );

            comm.SccCompileRequest msg = new comm.SccCompileRequest()
            {
                fileName = System.IO.Path.Combine( root, file ),
                scriptsRoot = root
            };

            msg.code.AddRange( Encoding.UTF8.GetBytes( request.Text ) );

            connection.Send( msg );

            request.Started = true;
        }

        public Request GetFirstRequest( string file, bool started )
        {
            List<Request> requests = GetRequests( file );

            return requests.FirstOrDefault( r => r.Started == started );
        }

        public Request CompilationComplete( string file, ulong hash, bool success )
        {
            List<Request> requests = GetRequests( file );

            for ( int i = requests.Count - 1; i >= 0; --i )
            {
                Request request = requests[ i ];

                if ( request.Hash == hash )
                {
                    Debug.Assert( request.Started );
                    Debug.Assert( !request.Compilation.IsCompleted );

                    request.CompilationCompletionSource.SetResult( success );

                    RemoveRequestIfComplete( requests, i );

                    return request;
                }
            }

            return null;
        }

        public Request TokensComplete( string file, ulong hash, bool success )
        {
            List<Request> requests = GetRequests( file );

            for ( int i = requests.Count - 1; i >= 0; --i )
            {
                Request request = requests[ i ];

                if ( request.Hash == hash )
                {
                    Debug.Assert( request.Started );
                    Debug.Assert( !request.Tokens.IsCompleted );

                    request.TokensCompletionSource.SetResult( success );

                    RemoveRequestIfComplete( requests, i );

                    return request;
                }
            }

            return null;
        }

        #endregion Public Methods

        #region Private Methods

        private void RemoveRequestIfComplete( List<Request> requests, int index )
        {
            Request request = requests[ index ];

            if ( request.Compilation.IsCompleted && request.Tokens.IsCompleted )
            {
                requests.RemoveAt( index );
            }
        }

        private List<Request> GetRequests( string file )
        {
            List<Request> requests;

            if ( !_requests.TryGetValue( file, out requests ) )
            {
                requests = new List<Request>();
                _requests[ file ] = requests;
            }

            return requests;
        }

        private async Task<ulong> CalculateHash( string data )
        {
            return await Task.Run( () => { return Hash.Fnv64( data, Encoding.UTF8 ); } );
        }

        [Conditional( "DEBUG" )]
        private void Log( string file, string message )
        {
            Debug.Print( $"[Parse Requests] ({file} / {System.Threading.Thread.CurrentThread.ManagedThreadId}): {message}" );
        }

        #endregion Private Methods
    }


    internal class FileParser
    {
        #region Private Data

        private Defines.Connection _connection;
        private Stub.Database _database;

        private LockedRequests _requests = new LockedRequests();

        #endregion Private Data

        #region Public Properties

        public ILogPane Log { get; set; }

        #endregion Public Properties

        #region C-Tor

        public FileParser( Defines.Connection connection, Stub.Database database )
        {
            _connection = connection;
            _database = database;

            _connection.MessageHandlers[ comm.SccCompileFileResponse.GetStaticID() ] = OnCompileRecieved;
            _connection.MessageHandlers[ comm.SccCompileFunctionsResponse.GetStaticID() ] = OnTokensRecieved;
        }

        #endregion C-Tor

        #region Public Methods

        public async Task Parse( string file, ITextSnapshot data )
        {
            await ParseInternal( file, data );
        }

        #endregion Public Methods

        #region Private Methods

        private async Task ParseInternal( string absolute, ITextSnapshot data )
        {
            Debug.Print( $"[FileParser] ({absolute} / {System.Threading.Thread.CurrentThread.ManagedThreadId}): Request" );

            FilePath path = new FilePath() { Absolute = absolute };

            string root = path.Root;
            if ( string.IsNullOrEmpty( root ) )
            {
                Log?.PrintLine( $"Could not resolve root scripts directory using file path: {absolute}" );

                DebugLog( absolute, "Could not resolve root scripts directory, exiting" );
                return;
            }

            string relative = path.Relative;

            await _requests.CreateRequest( relative, data );
            await _requests.AwaitInprogressRequests( relative );
            await _requests.SendNextRequest( relative, root, _connection );
        }

        [Conditional( "DEBUG" )]
        private void DebugLog( string file, string message )
        {
            Debug.Print( $"[FileParser] ({file} / {System.Threading.Thread.CurrentThread.ManagedThreadId}): {message}" );
        }

        #endregion Private Methods

        #region Event Handlers

        private async void OnCompileRecieved( comm.Message message )
        {
            comm.SccCompileFileResponse response = (comm.SccCompileFileResponse)message;

            DebugLog( response.file.path, "Compilation message received" );

            Request request = await _requests.CompilationComplete( response.file.path, response.file.fnv64, response.success );

            if ( response.success )
                _database.Add( response.file.path, response.database, request.Data, false );
        }

        private async void OnTokensRecieved( comm.Message message )
        {
            comm.SccCompileFunctionsResponse tokens = (comm.SccCompileFunctionsResponse)message;

            DebugLog( tokens.file.path, "Tokens message received" );

            Request request = await _requests.TokensComplete( tokens.file.path, tokens.file.fnv64, tokens.success );

            if ( tokens.success )
                _database.Add( tokens.file.path, tokens, request.Data );
        }

        #endregion Event Handlers
    }
}
